//
//  APIClient.swift
//  Link
//
//  Created by melvyn on 2018/11/24.
//

import Foundation
import Alamofire


let kApiRelativeURL = "http://192.168.31.88:8181"

open class APIClient {
    
    public struct Route: RawRepresentable {
        
        public var rawValue: String
        
        public init(rawValue: String) {
            self.rawValue = rawValue
        }
    }
    
    
    /// User ID for current active user.
    public let userID: AuthSession.ID

    /// The default client with gest session.
    public static var `default`: APIClient = {
        return APIClient.init(userID: -1)
    }()
    
    public init(userID: AuthSession.ID) {
        self.userID = userID
    }
    
    private func upload(
        multipartFormData: @escaping (MultipartFormData) -> Void,
        usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold,
        to url: URLConvertible,
        method: HTTPMethod = .post,
        headers: HTTPHeaders? = nil,
        encodingCompletion: ((SessionManager.MultipartFormDataEncodingResult) -> Void)?)
    {
        
        if isLoggedIn, let session = Link.shared.sessionStore.session {
            SessionManager.default.adapter = TokenAdapter.init(accessToken: session.token)
        } else {
            SessionManager.default.adapter = nil
        }
        
        return Alamofire.upload(
            multipartFormData: multipartFormData,
            usingThreshold: encodingMemoryThreshold,
            to: url,
            method: method,
            headers: headers,
            encodingCompletion: encodingCompletion
        )
    }
    
    private func request(
        _ url: URLConvertible,
        method: HTTPMethod = .get,
        parameters: Parameters? = nil,
        encoding: ParameterEncoding = URLEncoding.default,
        headers: HTTPHeaders? = nil) -> DataRequest {
        
        if isLoggedIn, let session = Link.shared.sessionStore.session {
            SessionManager.default.adapter = TokenAdapter.init(accessToken: session.token)
        } else {
            SessionManager.default.adapter = nil
        }
        
        return Alamofire.request(
            url,
            method: method,
            parameters: parameters,
            encoding: encoding,
            headers: headers
            )
            .validate(statusCode: 200..<300)
    }
}

extension APIClient: API {
    
    public var isLoggedIn: Bool {
        guard userID != -1 else {
            return false
        }
        return Link.shared.sessionStore.session?.userID == userID
    }
    
    
    public func requestUser(_ uid: User.ID, completionHandler: @escaping (Result<User>) -> Void) -> DataRequest {
        return request(Route.users.description + "/\(uid)").responseEntity(modelClass: User.self, completionHandler: {
            completionHandler($0.result)
        })
    }
    
    
    public func openLiveStream(
        _ multipartForm: LiveStream.MultipartForm,
        completionHandler: @escaping (Result<LiveStream>) -> Void) {
        guard isLoggedIn else {
            completionHandler(.failure(APIError.autheticatedFailed(reason: .unauthorized)))
            return
        }
        
        upload(multipartFormData: { (multipartFormData) in
            multipartFormData.encode(multipartForm)
        }, to: Route.streams.description) {
            switch $0 {
            case .success(let upload, _, _):
                
                upload.responseEntity(modelClass: LiveStream.self, completionHandler: { response in
                    
                    guard response.result.isSuccess else {
                        completionHandler(.failure(AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength)))
                        return
                    }
                    completionHandler(response.result)
                })
            case .failure(let error):
                completionHandler(.failure(error))
            }
        }
    }

    
    @discardableResult
    public func shutdownLiveStream(
        _ liveStream: LiveStream,
        completionHandler: @escaping (Result<Bool>) -> Void) -> DataRequest {
        let task = request(Route.streams.description + "/\(liveStream.id)", method: .delete)
        
        guard isLoggedIn else {
            completionHandler(.failure(APIError.autheticatedFailed(reason: .unauthorized)))
            return task
        }
        
        return task.response(completionHandler: {
            guard $0.response?.statusCode == 200 else {
                completionHandler(.failure(AFError.responseValidationFailed(reason: .dataFileNil)))
                return
            }
            completionHandler(.success(true))
        })
    }

    
    @discardableResult
    public func requestLiveStreams(completionHandler: @escaping (Result<[LiveStream]>) -> Void) -> DataRequest {
        return request(Route.streams.description).responseEntity(modelClass: [LiveStream].self) {
            completionHandler($0.result)
        }
    }

    
    @discardableResult
    public func showLiveStream(
        _ liveStream: LiveStream,
        completionHandler: @escaping (Result<LiveStream>) -> Void) -> DataRequest {
        return request(Route.streams.description + "/\(liveStream.id)").responseEntity(modelClass: LiveStream.self) { response in
            completionHandler(response.result)
        }
    }

    
    @discardableResult
    public func attemptAppRegistration(
        _ input: User.MultipartForm,
        completionHandler: @escaping (Result<Bool>) -> Void) -> DataRequest {
        
        guard let dictionaryValue = (try? JSONAdapter.jsonObject(from: input)) as? Parameters else {
            return request(Route.users.description, method: .post, encoding: JSONEncoding.default)
        }
        return request(Route.users.description, method: .post, parameters: dictionaryValue, encoding: JSONEncoding.default)
            .response { (_) in
                completionHandler(.success(true))
        }
    }
   
    
    @discardableResult
    public func attemptAppLogin(
        _ input: BasicAuthorization,
        completionHandler: @escaping (Result<AuthSession>) -> Void) -> DataRequest {
        let authorizationHeader = Request.authorizationHeader(user: input.username, password: input.password)!
        let headers = [authorizationHeader.key : authorizationHeader.value]
        
        return request(Route.session.description, method: .post, headers: headers).responseData {
            
            guard $0.result.isSuccess else {
                completionHandler(.failure($0.error!))
                return
            }
            
            do {
                guard let metadata = $0.result.value else {
                    throw AFError.responseSerializationFailed(reason: .inputDataNil)
                }
                let payload = try JSONDecoder().decode(AuthSession.self, from: metadata)
                
                Link.shared.sessionStore.save(payload, completionHandler: completionHandler)
                
            } catch let error {
                completionHandler(.failure(error))
            }
        }.responseJSON(completionHandler: { print($0) })
    }
 
    
    @discardableResult
    public func attemptAppLogout(completionHandler: @escaping (Result<AuthSession.ID>) -> Void) -> DataRequest {
        let task = request(Route.session.description, method: .delete)
        
        guard isLoggedIn else {
            completionHandler(.failure(APIError.autheticatedFailed(reason: .unauthorized)))
            return task
        }
        
        let userID = self.userID
        return task.response(completionHandler: { _ in
            Link.shared.sessionStore.logout(with: userID)
            completionHandler(.success(userID))
        })
    }
}

#if canImport(RxCocoa)
import RxSwift
import RxCocoa

extension APIClient: ReactiveCompatible {}

extension Reactive: RxAPI where Base: APIClient {
    public func requestUser(_ uid: User.ID) -> Observable<Result<User>> {
        return Observable.create({ [weak base](observer) -> Disposable in
            let task = base?.requestUser(uid, completionHandler: {
                observer.onNext($0)
                observer.onCompleted()
            })
            return Disposables.create {
                task?.cancel()
            }
        })
    }
    
    
    public func openLiveStream(_ multipartForm: LiveStream.MultipartForm) -> Observable<Result<LiveStream>> {
        return Observable.create({ [weak base](observer) -> Disposable in
            base?.openLiveStream(multipartForm, completionHandler: {
                observer.onNext($0)
                observer.onCompleted()
            })
            return Disposables.create()
        })
    }
    
    
    public func shutdownLiveStream(_ liveStream: LiveStream) -> Observable<Result<Bool>> {
        return Observable.create({ [weak base](observer) -> Disposable in
            base?.shutdownLiveStream(liveStream, completionHandler: {
                observer.onNext($0)
                observer.onCompleted()
            })
            return Disposables.create()
        })
    }
    
    
    public func requestLiveStreams() -> Observable<Result<[LiveStream]>> {
        return Observable.create({ [weak base](observer) -> Disposable in
            let task = base?.requestLiveStreams(completionHandler: {
                observer.onNext($0)
                observer.onCompleted()
            })
            return Disposables.create {
                task?.cancel()
            }
        })
    }
    
    
    public func showLiveStream(_ liveStream: LiveStream) -> Observable<Result<LiveStream>> {
        return Observable.create({ [weak base](observer) -> Disposable in
            let task = base?.showLiveStream(liveStream, completionHandler: {
                observer.onNext($0)
                observer.onCompleted()
            })
            
            return Disposables.create {
                task?.cancel()
            }
        })
    }
    
    
    public func attemptAppRegistration(_ input: User.MultipartForm) -> Observable<Result<Bool>> {
        return Observable.create({ [weak base](observer) -> Disposable in
            let task = base?.attemptAppRegistration(input) {
                observer.onNext($0)
                observer.onCompleted()
            }
            
            return Disposables.create {
                task?.cancel()
            }
        })
    }
    
    
    public func attemptAppLogin(_ input: BasicAuthorization) -> Observable<Result<AuthSession>> {
        return Observable.create({ [weak base](observer) -> Disposable in
            let task = base?.attemptAppLogin(input) { (response) in
                observer.onNext(response)
                observer.onCompleted()
            }
            
            return Disposables.create {
                task?.cancel()
            }
        })
    }
    
    
    public func attemptAppLogout() -> Observable<Result<AuthSession.ID>> {
        return Observable.create({ [weak base](observer) -> Disposable in
            let task = base?.attemptAppLogout(completionHandler: {
                observer.onNext($0)
                observer.onCompleted()
            })
            return Disposables.create {
                task?.cancel()
            }
        })
    }
}
#endif

extension APIClient.Route {
    public static let users = APIClient.Route.init(rawValue: "/api/users")
    public static let streams = APIClient.Route.init(rawValue: "/api/lives")
    public static let session = APIClient.Route.init(rawValue: "/api/session")
    public static let images = APIClient.Route.init(rawValue: "/api/images/")
    public static let RTMP = APIClient.Route.init(rawValue: "rtmp://192.168.31.88/live/")
}
extension APIClient.Route: CustomStringConvertible {
    public var description: String {
        if self == APIClient.Route.RTMP {
            return path
        }
        return kApiRelativeURL + path
    }
    
    var path: String {
        return rawValue
    }
}

extension APIClient: Equatable {
    public static func == (lhs: APIClient, rhs: APIClient) -> Bool {
        return lhs.userID == rhs.userID
    }
}

extension APIClient: CustomStringConvertible {
    public var description: String {
        return """
        class:  APIClient
        userID: \(userID)
        routes:
        \(Route.users)
        \(Route.streams)
        \(Route.session)
        \(Route.images)
        \(Route.RTMP)
        """
    }
}

